#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
    Part of Seagull - The Pyhon Pages engine
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Python Page Compiler -- Code generator
"""
__author__ = 'Jonathan Slenders <jonathan at slenders dot be>'
__license__ = 'GNU LGPL'
__all__ = ('CodeGenerator', )

import xml.dom.minidom

import parser
from compile_exceptions import CompileException

class CodeGenerator(object):
	"""
	Accepts and interprents the parser result. This class can generate
	the compiled code.
	"""
	def __init__(self, parse_tree, insert_debug_code=False):
		"""
		Transform this parsed python_page in a CodeGenerator object.
		"""
		assert isinstance(parse_tree, parser.PythonPage)

		self._parse_tree = parse_tree
		self._whitespace_compression = parse_tree.whitespace_compression
		self._insert_debug_code = insert_debug_code
		self._my_content_blocks = [ ]
		self._embedded_pages = [ ] # [ EmbeddedPage ]
		self._placeholders = [ ] # [ name:str ]
		self._embedded_page_varname_counter = 0
		self._block_id_counter = 0

		# Find all blocks, and translate to real python syntax
		self._init_block = self._compile_block(parse_tree.init_block, skip_literals=True)

		for block in parse_tree.content_blocks:
			self._my_content_blocks.append(self._compile_block(block))

	def _compile_block(self, parser_block, function_name_prefix='', skip_literals=False):
		block = Block(parser_block, function_name_prefix=function_name_prefix, insert_debug_code=self._insert_debug_code)

		for c in parser_block.content:
			if isinstance(c, basestring):
				if not skip_literals:
					block.append_literal(c, whitespace_compression=self._whitespace_compression)

			elif isinstance(c, parser.PythonCode):
				if c.print_result_escaped:
					for code in ('__page__.sys.stdout.write', '(', '__page__.esc', '('):
						block.append_code(code)

				for code in c.code:
					if isinstance(code, parser.PythonCode.String):
						block.append_python_string(code.content)

					elif isinstance(code, parser.PythonCode.Comment):
						pass

					elif isinstance(code, parser.PythonCode.Code):
						if code.content == ':':
							block.append_python_colon(code.line, code.column)
						elif code.content == ';':
							block.append_python_semi_colon(code.line, code.column)
						elif not code.content.isspace():
							block.append_code(code.content)
					else: raise

				if c.print_result_escaped:
					for code in (')', ')', ';'):
						block.append_code(code)

			elif isinstance(c, parser.EmbeddedPage):
				# Generate object varname when none has been defined
				if c.object_varname == None:
					c.object_varname = self._generate_embedded_page_varname()

				# add render() method to parent block
				if c.render in (None, True):
					entry = (c.entry if c.entry != None else 'Main')
					block.append_embedded_page(c.object_varname, entry)

				# Create new EmbeddedPage
				embedded_page = EmbeddedPage(c)
				self._embedded_pages.append(embedded_page)

				for b in c.content_blocks + [c.default_content_block]:
					if b:
						# Generate embedded content block name
						if b.name == None:
							b.name = self._generate_content_block_name()

						# Compile block
						embedded_page.content_blocks.append(self._compile_block(b, function_name_prefix=c.object_varname))

			elif isinstance(c, parser.Placeholder):
				name = (c.name if c.name else 'default')
				if not name in self._placeholders:
					self._placeholders.append(name)

				block.append_placeholder(name)

			elif isinstance(c, parser.Call):
				block.append_call(c.entry)

			elif isinstance(c, parser.ContentBlock.Comment):
				pass

			else:
				raise

		block.finish()
		return block

	def _generate_embedded_page_varname(self):
		self._embedded_page_varname_counter += 1
		return 'generated_page_variable__%s' % self._embedded_page_varname_counter

	def _generate_content_block_name(self):
		self._block_id_counter += 1
		return 'generated_content_block__%s' % self._block_id_counter

	def get_compiled_code(self, path):
		def add_slashes(str):
			if str == None:
				return ''

			# Escape slashes
			str = str.replace ("\\", "\\\\")
			# Escape single quotes
			str = str.replace ("'", "\\'")
			# Escape double quotes
			str = str.replace ('"', '\\"')
			return str

		o = []

		o.append(u"#!/usr/bin/env python\n")
		o.append(u"# -*- coding: utf-8 -*-\n\n")
		o.append(u"##############################################\n")
		o.append(u"# Auto generated code -- Don't modify by hand!\n")
		o.append(u"##############################################\n")
		o.append(u"\n# PATH= %s\n\n" % path)
		o.append(u"import WebPage\n")
		o.append(u"import sys\n")
		o.append(u"import __builtin__\n")

		o.append(u"class CompiledPage(WebPage.Page):\n")
		o.append(u"\tdef __init__(self, server, placeholder_implementations, parameters, caller=None, child=None, instance_id=None):\n")

		o.append(u"\t\tWebPage.Page.__init__(self,\n")
		o.append(u"\t\t\tserver = server,\n")
		o.append(u"\t\t\tcaller = caller,\n")
		o.append(u"\t\t\tchild = child,\n")
		o.append(u"\t\t\tpath = u'%s',\n" % add_slashes(path))
		o.append(u"\t\t\tinstance_id = instance_id,\n")
		o.append(u"\t\t\treceived_implementations = placeholder_implementations,\n")
		o.append(u"\t\t\treceived_parameters = parameters,\n")
		o.append(u"\t\t\tinit_block = self.__init_block,\n")
		o.append(u"\t\t\tload_included_objects = self._load_included_objects,\n")

		if self._parse_tree.external != None:
			o.append(u"\t\t\texternal = %s,\n" % str(self._parse_tree.external))

		if self._parse_tree.disabled != None:
			o.append(u"\t\t\tdisabled = %s,\n" % str(self._parse_tree.disabled))

		if self._parse_tree.output_mode:
			o.append(u"\t\t\toutput_mode = '%s',\n" % add_slashes(self._parse_tree.output_mode))

		if self._parse_tree.content_type:
			o.append(u"\t\tcontent_type = '%s',\n" % add_slashes(self._parse_tree.content_type))

		o.append(u"\t\t\tmeta = {\n")
		for p in self._parse_tree.meta:
			o.append(u"\t\t\t\t'%s': '%s',\n" % (add_slashes(p.name), add_slashes(p.value)))
		o.append(u"\t\t\t\t},\n")

		if self._parse_tree.master:
			o.append(u"\t\t\tmaster_path = u'%s',\n" % add_slashes(self._parse_tree.master.path))
			o.append(u"\t\t\tmaster_parameters = {\n")
			for p in self._parse_tree.master.parameters:
				o.append(u"\t\t\t\t'%s': '%s',\n" % (add_slashes(p), add_slashes(self._parse_tree.master.parameters[p])))
			o.append(u"\t\t\t},\n")

		o.append(u"\t\t\timplementations_for_my_master = {\n")
		for b in self._my_content_blocks:
			o.append(u"\t\t\t\t'%s': WebPage.ContentBlock('%s', self.%s, {" % (
						add_slashes(b.name), add_slashes(b.name), b.get_function_name()))
			for p in b.parameters:
				o.append(u"'%s': '%s'," % (p, b.parameters[p]))
			o.append(u"}),\n")
		o.append(u"\t\t\t\t},\n")

		o.append(u"\t\t\tparameters = {\n")
		for param in self._parse_tree.parameters:
			o.append(u"\t\t\t\t'%s': WebPage.Parameter('%s', '%s', '%s', u'%s'" %
					(add_slashes(param.name),
					add_slashes(param.name),
					add_slashes(param.classname),
					add_slashes(param.type),
					add_slashes(param.default_value)))
			if param.get_function:
				o.append(u", get_function=u'%s'" % add_slashes(param.get_function))
			if param.set_function:
				o.append(u", set_function=u'%s'" % add_slashes(param.set_function))
			o.append(u"),\n")
		o.append(u"\t\t\t\t},\n")

		o.append(u"\t\t\tfilters = [")
		for f in self._parse_tree.filters:
			parameters = []
			for para in f.parameters:
				parameters.append(u"'%s': '%s'," % (add_slashes(para), add_slashes(f.parameters[para])))
			o.append(u"\t\t\t\tWebPage.Filter('%s', { %s }),\n" % (add_slashes(f.name), u''.join(parameters)))
		o.append(u"\t\t\t\t],\n")

		o.append(u"\t\t\t)\n")

		# Load Includes
		o.append(u"\tdef _load_included_objects(self):\n")
		for p in self._embedded_pages:
			parameters = []
			for para in p.parameters:
				parameters.append(u"'%s': '%s'," % (add_slashes(para), add_slashes(p.parameters[para])))
			content_blocks = [ ]
			for b in p.content_blocks:
				content_blocks.append(u"'%s': WebPage.ContentBlock('%s', self._wrap_placeholder('%s', self.%s), {" % (
						add_slashes(b.name), add_slashes(b.name), add_slashes(b.name), b.get_function_name()))
				for p2 in b.parameters:
					content_blocks.append(u"'%s': '%s'," % (p2, b.parameters[p2]))
				content_blocks.append(u"}),\n")

			o.append(u"\t\tself.controls._add_control(WebPage.Control(self, self.resolve_page(u'%s'), { %s }, { %s }), '%s', %s)\n" % (
					add_slashes(p.path), u''.join(parameters), u''.join(content_blocks),
					add_slashes(p.object_varname), ("'%s'" % add_slashes(p.params_varname) if p.params_varname else None)))

		o.append(u"\t\tpass\n")

		# Initialisation block
		o.append(u"\t@staticmethod\n")
		o.append(u"\tdef __init_block():\n")
		o.append(self._init_block.get_code())
		o.append(u"\n\t\t__page__.register_locals(locals())\n")

		# Content blocks
		for b in [self._my_content_blocks] + [p.content_blocks for p in self._embedded_pages]:
			for b2 in b:
				o.append(u"\t@staticmethod\n")
				o.append(u"\tdef %s():\n" % b2.get_function_name())
				o.append(b2.get_code())
				o.append(u"\n\t\t__page__.register_locals(locals())\n")

		return ''.join(o)

	def get_reflexion_info(self):
		doc = xml.dom.minidom.Document()
		pageinfo = doc.createElement ('pageinfo')

		pageinfo.setAttribute('external', str(bool(self._parse_tree.external)))

		if self._parse_tree.master:
			pageinfo.setAttribute('master', self._parse_tree.master.path)

		if self._parse_tree.output_mode:
			pageinfo.setAttribute('output-mode', self._parse_tree.output_mode)

		pageinfo.setAttribute('debug', str(self._insert_debug_code))

		# TODO: check all this code once again for consistency and completeness

		for p in self._parse_tree.parameters:
			parameter = doc.createElement ('parameter')
			parameter.setAttribute('name', (p.name if p.name != None else ''))
			parameter.setAttribute('type', (p.type if p.type != None else ''))
			parameter.setAttribute('class', (p.classname if p.classname != None else ''))
			parameter.setAttribute('description', (p.description if p.description != None else ''))
			parameter.setAttribute('default-value', (p.default_value if p.default_value != None else ''))

			if p.get_function:
				parameter.setAttribute('get-function', p.get_function)
			if p.set_function:
				parameter.setAttribute('set-function', p.set_function)

			pageinfo.appendChild(parameter)

		for m in self._parse_tree.meta:
			meta = doc.createElement ('meta')
			meta.setAttribute('name', m.name)
			meta.setAttribute('value', m.value)
			pageinfo.appendChild(meta)

		for p in self._placeholders:
			placeholder = doc.createElement ('placeholder')
			placeholder.setAttribute('name', p)
			pageinfo.appendChild(placeholder)

		for p in self._embedded_pages:
			i_page = doc.createElement('embedded-page')
			i_page.setAttribute('object-varname', p.object_varname)
			i_page.setAttribute('path', p.path)

			if p.params_varname:
				i_page.setAttribute('params-dict-varname', p.params_varname)

			for pp in p.parameters:
				i_page_parameter = doc.createElement('parameter')
				i_page_parameter.setAttribute('name', pp)
				i_page_parameter.setAttribute('value', p.parameters[pp])
				i_page.appendChild(i_page_parameter)

			pageinfo.appendChild(i_page)

		for i in self._init_block.get_debug_interrupts():
			breakpoint = doc.createElement('breakpoint')
			breakpoint.setAttribute('line', str(i.line))
			breakpoint.setAttribute('column', str(i.column))
			pageinfo.appendChild(breakpoint)

		for b in [ self._my_content_blocks ] + [p.content_blocks for p in self._embedded_pages]:
			for b2 in b:
				for i in b2.get_debug_interrupts():
					breakpoint = doc.createElement('breakpoint')
					breakpoint.setAttribute('line', str(i.line))
					breakpoint.setAttribute('column', str(i.column))
					pageinfo.appendChild(breakpoint)

		for c in self._parse_tree.content_blocks:
			content = doc.createElement('content')
			content.setAttribute('name', c.name)
			pageinfo.appendChild(content)

		doc.appendChild(pageinfo)
		return doc.toxml(encoding='utf-8')


class Block:
	def __init__(self, parser_block, function_name_prefix='', insert_debug_code=False):
		self._parser_block = parser_block
		self._function_name_prefix = function_name_prefix

		self._insert_debug_code = insert_debug_code
		self._code = []
		self._debug_interrupts = []
		self._indentation_level = 2
		self._indentation_stack = []
		self._parenthesis_stack = []

		self._start_newline()

	def __getattr__(self, key):
		return getattr(self._parser_block, key)

	def get_function_name(self):
		return '__content_%s_%s' % (self._function_name_prefix, self._parser_block.name)

	def _start_newline(self):
		self._code.append('\n')
		for x in range(0, self._indentation_level):
			self._code.append('\t')

	def enter_python_code(self, line, column):
		self._insert_debug_interrupt(line, column)

	def append_code(self, code):
		if code in ('(', '[', '{'):
			self._code.append(code)
			self._parenthesis_stack.append(code)
		elif code in (')', ']', '}'):
			if len(self._parenthesis_stack) and ( \
					(code == ')' and self._parenthesis_stack[-1] == '(') or \
					(code == ']' and self._parenthesis_stack[-1] == '[') or \
					(code == '}' and self._parenthesis_stack[-1] == '{') ):
				self._code.append(code)
				self._parenthesis_stack.pop()
			else:
				raise CompileException(0,0, 'Unmatched parenthesis')

		# NOTE: we should check the stack length because of  " [ for x in array ] "
		elif not len(self._parenthesis_stack) and  code in ('if', 'while', 'class', 'def', 'for', 'try'):
			self._code.append(u'%s ' % code)
			self._indentation_level += 1
			self._indentation_stack.append(code)

		elif not len(self._parenthesis_stack) and code in ('elif', 'else', 'except', 'finally'):
			self._indentation_level -= 1
			self._start_newline()
			self._code.append(u'%s ' % code)
			self._indentation_level += 1

		elif not len(self._parenthesis_stack) and len(self._indentation_stack) > 0 and code == \
				'end%s' % self._indentation_stack[-1]:
			self._indentation_level -= 1
			self._indentation_stack.pop()
			self._start_newline()
		else:
			self._code.append(u'%s ' % code)

	def append_python_colon(self, line, column):
		self._code.append(':')

		if not len(self._parenthesis_stack):
			self._start_newline()
			self._insert_debug_interrupt(line, column)

	def append_python_semi_colon(self, line, column):
		if len(self._parenthesis_stack):
			raise CompileException(0,0, 'Semicolon not allowed in this context')

		self._start_newline()
		self._insert_debug_interrupt(line, column)

	def _insert_debug_interrupt(self, line, column):
		if self._insert_debug_code:
			self._debug_interrupts.append(CodeGenerator.DebugInterrupt(line, column))
			self._code.append(u'__server__.debug_interrupt(__page__.path, %s, %s, __builtin__.globals(), __builtin__.locals())' %
					(line, column))
			self._start_newline()

	def append_python_string(self, code):
		# Note that the parser/lexer makes sure we have an escaped string after parsing
		# We don't have to escape it here
		self._code.append(u'u"""%s""" ' % code)
			# TODO: Should we insert a space before the 'u', probably not.
			# At least, we shouldn't when this token is the first in a line (e.g. for doc strings)

	def append_literal(self, literal, whitespace_compression=False):
		self._start_newline()

		if literal.isspace() and whitespace_compression:
			self._code.append('__page__.sys.stdout.write(u" ")')
		else:
			self._code.append('__page__.sys.stdout.write(u"""%s""")' %
				literal.replace('\\', '\\\\').replace('"', r'\"'))

		self._start_newline()

	def append_placeholder(self, placeholder_name):
		self._start_newline()
		self._code.append("__page__.try_call_placeholder(u'%s')" % placeholder_name) # TODO: escape
		self._start_newline()

	def append_call(self, entry):
		self._start_newline()
		self._code.append("__page__.execute(entry=u'%s')" % entry) # TODO: escape
		self._start_newline()

	def append_embedded_page(self, included_page_object_varname, entry):
		self._start_newline()
		self._code.append("__page__.register_locals(locals())")
		self._start_newline()
		self._code.append("__controls__.%s.execute(entry='%s')" % (included_page_object_varname, entry)) # TODO: escape
		self._start_newline()

	def finish(self):
		if len(self._indentation_stack) > 0:
			raise CompileException(0, 0, 'End tag for "%s" not closed' %
						self._indentation_stack[-1])

	def get_code(self):
		return ''.join(self._code)

	def get_debug_interrupts(self):
		return self._debug_interrupts

class EmbeddedPage(object):
	"""
	Extension to the parser.EmbeddedPage
	"""
	def __init__(self, parser_embedded_page):
		self._parser_embedded_page = parser_embedded_page
		self.content_blocks = [ ]

	def __getattr__(self, key):
		return getattr(self._parser_embedded_page, key)
